﻿/**
    Buttons are the foundation component of the CLIK framework and are used anywhere a clickable interface control is required. The default Button class (gfx.controls.Button) supports a textField to display a label, and states to visually indicate user interaction. Buttons can be used on their own, or as part of a composite component, such as ScrollBar arrows or the Slider thumb. Most interactive components that are click-activated compose or extend Button.
    
    The CLIK Button is a general purpose button component, which supports mouse interaction, keyboard interaction, states and other functionality that allow it to be used in a multitude of user interfaces. It also supports toggle capability as well as animated states. The ToggleButton, AnimatedButton and AnimatedToggleButton provided in the Button.fla component source file all use the same base component class.
    
    <b>Inspectable Properties</b>
    The inspectable properties of the Button component are:
    <ul>
        <li><i>autoRepeat</i>: Determines if the button dispatches "click" events when pressed down and held. </li>
        <li><i>autoSize</i>: Determines if the button will scale to fit the text that it contains and which direction to align the resized button. Setting the autoSize property to {@code autoSize="none"} will leave its current size unchanged.</li>
        <li><i>data</i>: Data related to the button. This property is particularly helpful when using butons in a ButtonGroup. </li>
        <li><i>enabled</i>: Disables the button if set to false.</li>
        <li><i>focusable</i>: By default buttons receive focus for user interactions. Setting this property to false will disable focus acquisition.</li>
        <li><i>label</i>: Sets the label of the Button.</li>
        <li><i>selected</i>: Set the selected state of the Button. Buttons can have two sets of mouse states, a selected and unselected.  When a Button's {@code toggle} property is {@code true} the selected state will be changed when the button is clicked, however the selected state can be set using ActionScript even if the toggle property is false.</li>
        <li><i>toggle</i>: Sets the toggle property of the Button. If set to true, the Button will act as a toggle button.</li>
        <li><i>visible</i>: Hides the button if set to false.</li>
    </ul>
    
    <b>States</b>
    The CLIK Button component supports different states based on user interaction. These states include
    <ul>
        <li>an up or default state.</li>
        <li>an over state when the mouse cursor is over the component, or when it is focused.</li>
        <li>a down state when the button is pressed.</li>
        <li>a disabled state.</li>
    </ul>
    
    These states are represented as keyframes in the Flash timeline, and are the minimal set of keyframes required for the Button component to operate correctly. There are other states that extend the capabilities of the component to support complex user interactions and animated transitions, and this information is provided in the Getting Started with CLIK Buttons document.    
    
    <b>Events</b>
    All event callbacks receive a single Event parameter that contains relevant information about the event. The following properties are common to all events. <ul>
    <li><i>type</i>: The event type.</li>
    <li><i>target</i>: The target that generated the event.</li></ul>
        
    The events generated by the Button component are listed below. The properties listed next to the event are provided in addition to the common properties.
    <ul>
        <li><i>ComponentEvent.SHOW</i>: The visible property has been set to true at runtime.</li>
        <li><i>ComponentEvent.HIDE</i>: The visible property has been set to false at runtime.</li>
        <li><i>FocusHandlerEvent.FOCUS_IN</i>: The button has received focus.</li>
        <li><i>FocusHandlerEvent.FOCUS_OUT</i>: The button has lost focus.</li>
        <li><i>Event.SELECT</i>: The selected property has changed.</li>
        <li><i>ComponentEvent.STATE_CHANGE</i>: The button's state has changed.</li>
        <li><i>ButtonEvent.PRESS</i>: The button has been pressed.</li>
        <li><i>ButtonEvent.CLICK</i>: The button has been clicked.</li>
        <li><i>ButtonEvent.DRAG_OVER</i>: The mouse cursor has been dragged over the button (while the left mouse button is pressed).</li>
        <li><i>ButtonEvent.DRAG_OUT</i>: The mouse cursor has been dragged out of the button (while the left mouse button is pressed).</li>
        <li><i>ButtonEvent.RELEASE_OUTSIDE</i>: The mouse cursor has been dragged out of the button and the left mouse button has been released.</li>
    </ul>
*/

/**************************************************************************

Filename    :   Button.as

Copyright   :   Copyright 2011 Autodesk, Inc. All Rights reserved.

Use of this software is subject to the terms of the Autodesk license
agreement provided at the time of installation or download, or which
otherwise accompanies this software in either electronic or hard copy form.

**************************************************************************/
 
package scaleform.clik.controls {
    
    import flash.display.DisplayObject;
    import flash.display.MovieClip;
    import flash.events.Event;
    import flash.events.MouseEvent;
    import flash.events.TimerEvent;
    import flash.text.TextField;
    import flash.text.TextFieldAutoSize;
    import flash.text.TextFormat;
    import flash.utils.Timer;
    import flash.utils.getTimer;
    
    import scaleform.clik.constants.ConstrainMode;
    import scaleform.clik.constants.ControllerType;
    import scaleform.clik.constants.InputValue;
    import scaleform.clik.constants.InvalidationType;
    import scaleform.clik.constants.NavigationCode;
    import scaleform.clik.core.UIComponent;
    import scaleform.clik.events.ButtonEvent;
    import scaleform.clik.events.ComponentEvent;
    import scaleform.clik.events.InputEvent;
    import scaleform.clik.ui.InputDetails;
    import scaleform.clik.utils.ConstrainedElement;
    import scaleform.clik.utils.Constraints;
    import scaleform.gfx.FocusEventEx;
    import scaleform.gfx.MouseEventEx;
    
    [Event(name="select", type="flash.events.Event")]
    [Event(name="stateChange", type="scaleform.clik.events.ComponentEvent")]
    [Event(name="dragOver", type="scaleform.clik.events.ButtonEvent")]
    [Event(name="dragOut", type="scaleform.clik.events.ButtonEvent")]
    [Event(name="releaseOutside", type="scaleform.clik.events.ButtonEvent")]
    [Event(name="buttonRepeat", type="scaleform.clik.event.ButtonEvent")]
    
    public class Button extends UIComponent {
        
    // Constants:
        
    // Public Properties:
        /** Locks drag over and drag out state changes. Useful for scrollbar and slider thumbs. */
        public var lockDragStateChange:Boolean = false;
        /** The delay (milliseconds) before the initial repeat event is fired. */
        public var repeatDelay:Number = 500;
        /** The delay (milliseconds) between subsequent repeat events. */
        public var repeatInterval:Number = 200;
        /** True if constraints are disabled for the component. Setting the disableConstraintsproperty to {@code disableConstraints=true} will remove constraints from the textfield. This is useful for components with timeline based textfield size tweens, since constraints break them due to a Flash quirk. */
        public var constraintsDisabled:Boolean = false;
        /** True if the Button can be deselected when already selected and clicked again. False for RadioButtons by default.*/
        public var allowDeselect:Boolean = true;
        /** True if the Button should not autosize itself when the state changes. False otherwise-- by default. Note that the autoSize property will not work if preventAutosizing is set to true. */
        public var preventAutosizing:Boolean = false;
        
    // Protected Properties:
        protected var _toggle:Boolean = false;
        protected var _label:String;
        protected var _state:String;
        protected var _group:ButtonGroup;
        protected var _groupName:String;
        protected var _selected:Boolean = false;
        protected var _data:*;
        protected var _autoRepeat:Boolean = false;
        protected var _autoSize:String = TextFieldAutoSize.NONE;
        protected var _pressedByKeyboard:Boolean = false;
        protected var _isRepeating:Boolean = false;
        
        /** Reference to the UIComponent that generated this Button, if applicatable (eg. ButtonGroup). */
        protected var _owner:UIComponent = null;
        
        /** A list of frames that apply to a given state. The frames will be called in order, and the last existing frame will be displayed. */
        protected var _stateMap:* = {
            up:["up"],
            over:["over"],
            down:["down"],
            release: ["release", "over"],
            out:["out","up"],
            disabled:["disabled"],
            selecting: ["selecting", "over"],
            toggle: ["toggle", "up"],
            kb_selecting: ["kb_selecting", "up"],
            kb_release: ["kb_release", "out", "up"],
            kb_down: ["kb_down", "down"]
        }
        protected var _newFrame:String;
        protected var _newFocusIndicatorFrame:String;
        protected var _repeatTimer:Timer;
        protected var _mouseDown:int = 0; // Determines if the mouse is in a press for each controller.
        protected var _focusIndicatorLabelHash:*;
        protected var _autoRepeatEvent:ButtonEvent;
        
    // UI Elements:
        /** A reference to the textField in the component.  The textField is an optional element. Note that when state changes are made, the textField instance may change, so changes made to it externally may be lost. */
        public var textField:TextField;
        public var defaultTextFormat:TextFormat;
        
        /** A reference to a MovieClip that is used to denote focus. It can either have one frame (which will cause the Button to show/hide this focus movie by toggling its visibility), or have two named frames: show and hide, which will be played appropriately.*/
        protected var _focusIndicator:MovieClip;
        
    // Initialization:
        public function Button() {
            super();
        }
        
        override protected function preInitialize():void {
            if (!constraintsDisabled) {
                constraints = new Constraints(this, ConstrainMode.COUNTER_SCALE);
            }
        }
        
        override protected function initialize():void {
            super.initialize();
            
            // Set tabEnabled to true in ctor so that it's true for configUI(). This allows user to change tabEnabled 
            // before configUI() occurs and ensures that Button is tabEnabled == true; by default.
            tabEnabled = true; 
        }
        
    // Public getter / setters:
        /** Data related to the button. This property is particularly helpful when using butons in a ButtonGroup. */
        [Inspectable(type = "string", defaultValue = "")]
        public function get data():* { return _data; }
        public function set data(value:*):void {
            _data = value;
        }

        /** Determines if the button dispatches "click" events when pressed down and held. */
        [Inspectable(defaultValue = "false")]
        public function get autoRepeat():Boolean { return _autoRepeat; }
        public function set autoRepeat(value:Boolean):void {
            _autoRepeat = value;
        }
        
        /**
         * Enable/disable this component. Focus (along with keyboard events) and mouse events will be suppressed if disabled.
         */
        [Inspectable(defaultValue="true")]
        override public function get enabled():Boolean { return super.enabled; }
        override public function set enabled(value:Boolean):void {
            super.enabled = value;
            mouseChildren = false; // Keep mouseChildren false for Button and its subclasses.
            
            var state:String;
            if (super.enabled) {
                state = (_focusIndicator == null && (_displayFocus || _focused)) ? "over" : "up";
            } else {
                state = "disabled";
            }
            setState(state);
        }
        
        /**
         * Enable/disable focus management for the component. Setting the focusable property to 
         * {@code focusable=false} will remove support for tab key, direction key and mouse
         * button based focus changes.
         */
        [Inspectable(defaultValue="true")]
        override public function get focusable():Boolean { return _focusable; }
        override public function set focusable(value:Boolean):void { 
            super.focusable = value;
        }
        
        /** A button with a toggle value of {@code true} will change its selected state when the button is "clicked". */
        [Inspectable(defaultValue="false")]
        public function get toggle():Boolean { return _toggle; }
        public function set toggle(value:Boolean):void {
            _toggle = value;
        }
        
        /** Reference to the owner of this Button (generally, a subclass of CoreList). */
        public function get owner():UIComponent { return _owner; }
        public function set owner(value:UIComponent):void { _owner = value; }
        
        /** Retrieve the current state of the Button. */
        public function get state():String { return _state; }
    
        /**
         * Set the selected state of the Button. Buttons can have two sets of mouse states, a selected and unselected.
         * When a Button's {@code toggle} property is {@code true} the selected state will be changed when the button 
         * is clicked, however the selected state can be set using ActionScript even if the toggle property is false.
         */
        [Inspectable(defaultValue="false")]
        public function get selected():Boolean { return _selected; }
        public function set selected(value:Boolean):void {
            if (_selected == value) { return; }
            _selected = value;
            
            //#state This will do a change in state.
            //  1. If the component is not focused, and the selected property is changed via code, it will do a hard frame change unless it is part of a button group.
            //  2. If the component was pressed with the mouse, and is changing its selected state (toggle), it plays the "switch" animation.
            //  2b. If the component was pressed with the keyboard, but has no focusMovie, the "switch" animation plays.
            //  3. If the component was pressed with the keyboard (and is focused), it plays the kb_switch animation.
            //  4. EDGE CASE The component will not be able to be do a up->focused_selected_up when a focused clip changes its selected state via code.
            if (enabled) {
                if (!_focused) {
                    setState("toggle");
                }
                else if (_pressedByKeyboard && _focusIndicator != null) { 
                    // Pressed with keyboard + focusIndicator (down->selected_up, selected_down->up)
                    setState("kb_selecting");
                }
                else {
                    // Pressed with mouse or keyboard and no focusIndicator (down->selected_over, selected_down->over)
                    setState("selecting");
                }
                
                if (owner) {
                    // Use temp bool rather than _displayFocus to avoid redudancy check in set displayFocus().
                    var df:Boolean = (_selected && owner != null && checkOwnerFocused());
                    // If we have a focusIndicator and our owner is focused, use the "toggle" state. Otherwise, use "selecting".
                    setState( (df && _focusIndicator == null) ? "selecting" : "toggle" );
                    displayFocus = df
                }
            }
            else {
                setState("disabled");
            }
            
            validateNow();
            
            // The event is dispatched after the frame change so that listening objects can override the behavior.
            dispatchEvent(new Event(Event.SELECT));
        }
        
        /**
         * A reference to the {@link ButtonGroup} instance that the button belongs to. The group is usually created
         * in the parent clip of the button, so buttons in the same MovieClip scope with the same name can behave as
         * a group. ButtonGroups will only be created in the parent scope when automatically created.
         * @see ButtonGroup
         */
        public function get group():ButtonGroup { return _group; }
        public function set group(value:ButtonGroup):void {
            if (_group != null) { _group.removeButton(this); }
            _group = value;
            if (_group != null) { _group.addButton(this); }
        }
        
        /**
         * The name of the {@link #group} that the button belongs to. If the group does not exist, it is created in the parent of the button so that other buttons with the same group name can belong to the same group.
         * @see #group
         * @see ButtonGroup
         */
        public function get groupName():String { return _groupName; }
        public function set groupName(value:String):void {
            if (_inspector && value == "") { return; }
            if (_groupName == value) { return; }
            if (value != null) {
                addEventListener(Event.ADDED, addToAutoGroup, false, 0, true);
                addEventListener(Event.REMOVED, addToAutoGroup, false, 0, true);
            } else {
                removeEventListener(Event.ADDED, addToAutoGroup, false);
                removeEventListener(Event.REMOVED, addToAutoGroup, false);
            }
            _groupName = value;
            addToAutoGroup(null);
        }
        
        /**
         * The ActionScript-only label parameter that sets the text directly, and assumes localization
         * has been handled externally.
         */
        [Inspectable(defaultValue="")]
        public function get label():String { return _label; }
        public function set label(value:String):void {
            if (_label == value)	return;
            _label = value;
            invalidateData();
        }
        
        /**
         * Determines if the button will scale to fit the text that it contains. Setting the autoSize 
         * property to {@code autoSize="none"} will leave its current size unchanged.
         */
        [Inspectable(defaultValue="none", enumeration="none,left,right,center")]
        public function get autoSize():String { return _autoSize; }
        public function set autoSize(value:String):void {
            if (value == _autoSize) { return; }
            _autoSize = value;
            invalidateData();
        }
        
        /** @exclude */
        public function get focusIndicator():MovieClip { return _focusIndicator; }
        public function set focusIndicator(value:MovieClip):void {
            _focusIndicatorLabelHash = null;
            _focusIndicator = value;
            _focusIndicatorLabelHash = UIComponent.generateLabelHash(_focusIndicator);
        }
        
    // Public Methods:
        /** @exclude */
        override public function handleInput(event:InputEvent):void {
            if (event.isDefaultPrevented()) { return; }
            var details:InputDetails = event.details;
            var index:uint = details.controllerIndex;
            
            switch (details.navEquivalent) {
                case NavigationCode.ENTER:
                    if (details.value == InputValue.KEY_DOWN) {
                        handlePress(index);
                        event.handled = true;
                    }
                    else if (details.value == InputValue.KEY_UP) {
                        if (_pressedByKeyboard) { 
                            handleRelease(index);
                            event.handled = true;
                        }
                    }
                    break;
            }
        }
        
        /** @exclude */
        override public function toString():String { 
            return "[CLIK Button " + name + "]";
        }
        
    // Protected Methods:
        override protected function configUI():void {
            if (!constraintsDisabled)	constraints.addElement("textField", textField, Constraints.ALL);
            
            tabEnabled = (_focusable && enabled && tabEnabled);
            mouseChildren = tabChildren = false;
            
            addEventListener(MouseEvent.ROLL_OVER, handleMouseRollOver, false, 0, true);
            addEventListener(MouseEvent.ROLL_OUT, handleMouseRollOut, false, 0, true);
            addEventListener(MouseEvent.MOUSE_DOWN, handleMousePress, false, 0, true);
            addEventListener(MouseEvent.CLICK, handleMouseRelease, false, 0, true);
            addEventListener(MouseEvent.DOUBLE_CLICK, handleMouseRelease, false, 0, true);
            addEventListener(InputEvent.INPUT, handleInput, false, 0, true);
            
            //LM: Consider moving to showFocus() or something similar.
            if (_focusIndicator != null && !_focused && _focusIndicator.totalFrames == 1) { focusIndicator.visible = false; }
        }
        
        override protected function draw():void {
            // State is invalid, and has been set (is not the default)
            if (isInvalid(InvalidationType.STATE)) {
                if (_newFrame) {
                    gotoAndPlay(_newFrame);
                    _newFrame = null;
                }
                
                if (_newFocusIndicatorFrame) {
                    focusIndicator.gotoAndPlay(_newFocusIndicatorFrame);
                    _newFocusIndicatorFrame = null;
                }
                
                updateAfterStateChange();
                dispatchEvent(new ComponentEvent(ComponentEvent.STATE_CHANGE));
                // NFM: Should size be invalidated here by default? It can cause problems with timeline animations, 
                //      especially with focusIndictators that extend beyond the size of the original MovieClip. Instead, 
                //      perhaps let subclasses call invalidate(InvalidationType.SIZE) as necessary instead.
                invalidate(InvalidationType.DATA, InvalidationType.SIZE);
            }
            
            // Data is invalid when label or autoSize changes.
            if (isInvalid(InvalidationType.DATA)) {
                updateText();
                if (autoSize != TextFieldAutoSize.NONE) {
                    invalidateSize();
                }
            }
            
            // Resize and update constraints
            if (isInvalid(InvalidationType.SIZE) ) {
                if (!preventAutosizing) { 
                    alignForAutoSize();
                    setActualSize(_width, _height);
                }
                if (!constraintsDisabled) {
                    constraints.update(_width, _height);
                }
            }
        }
        
        protected function addToAutoGroup(event:Event):void {
            if (parent == null) {
                group = null;
                return;
            }
            var g:ButtonGroup = ButtonGroup.getGroup(_groupName, parent);
            if (g == group) { return; }
            group = g;
        }
        
        protected function checkOwnerFocused():Boolean {
            var ownerFocused:Boolean = false;
            if (owner != null) {
                ownerFocused = (_owner.focused != 0); // Is owner focused?
                
                // If the owner wasn't focused, check its .focusTarget (eg. ScrollingList in a DropDownMenu).
                if (ownerFocused == 0) { 
                    var ownerFocusTarget:* = _owner.focusTarget;
                    if (ownerFocusTarget != null) { 
                        ownerFocused = (ownerFocusTarget != 0)
                    }
                }
            }
            
            return ownerFocused;
        }
        
        protected function calculateWidth():Number {
            var w:Number = actualWidth;
            if (!constraintsDisabled) {
                var element:ConstrainedElement = constraints.getElement("textField");
                w = Math.ceil(textField.textWidth + element.left + element.right + 5); // We add 5 pixels to accommodate Flash's poor measurement of anti-aliased fonts.
            }
            
            return w;
        }
    
        protected function alignForAutoSize():void {
            if (!initialized || _autoSize == TextFieldAutoSize.NONE || textField == null) { return; }
            
            var oldWidth:Number = _width;
            var newWidth:Number = _width = calculateWidth();
            
            switch (_autoSize) {
                case TextFieldAutoSize.RIGHT:
                    var oldRight:Number = x + oldWidth;
                    x = oldRight - newWidth;
                    break;
                case TextFieldAutoSize.CENTER:
                    var oldCenter:Number = x + oldWidth * 0.5;
                    x = oldCenter - newWidth * 0.5;
                    break;
            }
        }
        
        protected function updateText():void {
            if (textField)	textField.text = _label || "";
        }
        
        override protected function changeFocus():void {
            if (!enabled) { return; }
            if (_focusIndicator == null) {
                setState((_focused || _displayFocus) ? "over" : "out");
                if (_pressedByKeyboard && !_focused) {
                    _pressedByKeyboard = false;
                }
            } else {
                if (_focusIndicator.totalframes == 1) { 
                    _focusIndicator.visible = _focused > 0;  // Do a simple show/hide on single-frame focus indicators.
                } else {
                    // Check if the focus state exists first, and use it. Otherwise use default behaviour.
                    var focusFrame:String = "state" + _focused;
                    if (_focusIndicatorLabelHash[focusFrame]) {
                        _newFocusIndicatorFrame = "state" + _focused;
                    } else {
                        _newFocusIndicatorFrame = (_focused || _displayFocus) ? "show" : "hide";
                    }
                    invalidateState();
                }
                // If focus is moved on keyboard press, the button needs to reset since it will not recieve a key up event.
                if (_pressedByKeyboard && !_focused) {
                    setState("kb_release");
                    _pressedByKeyboard = false;
                }
            }
        }
        
        // Mouse-only input
        protected function handleMouseRollOver(event:MouseEvent):void {
            var sfEvent:MouseEventEx = event as MouseEventEx;
            var mouseIdx:uint = (sfEvent == null) ? 0 : sfEvent.mouseIdx;
            
            // DRAG_OVER
            if (event.buttonDown) {
                dispatchEvent(new ButtonEvent(ButtonEvent.DRAG_OVER));
                
                if (!enabled) { return; }
                if (lockDragStateChange && Boolean(_mouseDown << mouseIdx & 1)) { return; }
                if (_focused || _displayFocus) {
                    setState(focusIndicator == null ? "down" : "kb_down"); // The user has dragged out first, so do a up->down transition.
                } else {
                    setState("over");
                }
            } 
            
            // MOUSE_OVER
            else {
                if (!enabled) { return; }
                if (_focused || _displayFocus) {
                    if (_focusIndicator != null) { setState("over"); }
                } else {
                    setState("over");
                }
            }
            
        }
        protected function handleMouseRollOut(event:MouseEvent):void {
            var sfEvent:MouseEventEx = event as MouseEventEx;
            var index:uint = (sfEvent == null) ? 0 : sfEvent.mouseIdx;
            
            // DRAG_OUT
            if (event.buttonDown) {
                dispatchEvent(new ButtonEvent(ButtonEvent.DRAG_OUT));
                if (Boolean(_mouseDown & 1 << index)) {
                    if (stage != null) { // Button may have just been removed from stage, firing a MouseRollOut. Stage will be null in this case.
                        stage.addEventListener(MouseEvent.MOUSE_UP, handleReleaseOutside, false, 0, true);
                    }
                }
                
                if (lockDragStateChange || !enabled) { return; }
                if (_focused || _displayFocus) {
                    setState(_focusIndicator == null ? "release" : "kb_release"); // The user has pressed and dragged out of the component. Do a down->up transition.
                } else {
                    setState("out");
                }
            }
            
            // ROLL_OUT
            else {
                if (!enabled) { return; }
                if (_focused || _displayFocus) {
                    if (_focusIndicator != null) { setState("out"); }
                }
                else { 
                    setState("out"); 
                }
            }
        }
        
        protected function handleMousePress(event:MouseEvent):void {
            var sfEvent:MouseEventEx = event as MouseEventEx;
            var mouseIdx:uint = (sfEvent == null) ? 0 : sfEvent.mouseIdx; // index of Mouse that generated event.
            var btnIdx:uint = (sfEvent == null) ? 0 : sfEvent.buttonIdx; // index of Button of Mouse that generated event.
            if (btnIdx != 0) { return; }
            
            _mouseDown |= 1 << mouseIdx;
            
            if (enabled) { 
                setState("down");
                
                // Uses single controller like AS2.
                if (autoRepeat && _repeatTimer == null) {
                    _autoRepeatEvent = new ButtonEvent(ButtonEvent.CLICK, true, false, mouseIdx, btnIdx, false, true);
                    _repeatTimer = new Timer(repeatDelay, 1);
                    _repeatTimer.addEventListener(TimerEvent.TIMER_COMPLETE, beginRepeat, false, 0, true);
                    _repeatTimer.start();
                }
                
                var sfButtonEvent:ButtonEvent = new ButtonEvent(ButtonEvent.PRESS, true, false, mouseIdx, btnIdx, false, false);
                dispatchEvent(sfButtonEvent);
            }
        }
        
        protected function handleMouseRelease(event:MouseEvent):void {
            _autoRepeatEvent = null;
            if (!enabled) { return; }
            var sfEvent:MouseEventEx = event as MouseEventEx;
            var mouseIdx:uint = (sfEvent == null) ? 0 : sfEvent.mouseIdx;
            var btnIdx:uint = (sfEvent == null) ? 0 : sfEvent.buttonIdx; // index of Button of Mouse that generated event.
            if (btnIdx != 0) { return; }
            
            _mouseDown ^= 1 << mouseIdx;
            
            // Always remove timer, in case autoRepeat was turned off during press.
            if (_mouseDown == 0 && _repeatTimer) {
                _repeatTimer.stop(); _repeatTimer.reset();
                _repeatTimer.removeEventListener(TimerEvent.TIMER_COMPLETE, beginRepeat);
                _repeatTimer.removeEventListener(TimerEvent.TIMER, handleRepeat);
                _repeatTimer = null; //LM: Consider leaving in memory.
            }
            
            setState("release");
            handleClick(mouseIdx);
            
            if (!_isRepeating) {
                var sfButtonEvent:ButtonEvent = new ButtonEvent(ButtonEvent.CLICK, true, false, mouseIdx, btnIdx, false, false);
                dispatchEvent(sfButtonEvent);
            }
            
            _isRepeating = false;
        }
        
        protected function handleReleaseOutside(event:MouseEvent):void {
            _autoRepeatEvent = null;
            if (contains(event.target as DisplayObject)) { return; }
            
            var sfEvent:MouseEventEx = event as MouseEventEx;
            var mouseIdx:uint = (sfEvent == null) ? 0 : sfEvent.mouseIdx;
            var btnIdx:uint = (sfEvent == null) ? 0 : sfEvent.buttonIdx; // index of Button of Mouse that generated event.
            if (btnIdx != 0) { return; }
            
            stage.removeEventListener(MouseEvent.MOUSE_UP, handleReleaseOutside, false);
            
            _mouseDown ^= 1 << mouseIdx;
            dispatchEvent(new ButtonEvent(ButtonEvent.RELEASE_OUTSIDE));
            if (!enabled) { return; }
            
            if (lockDragStateChange) { 
                if (_focused || _displayFocus) {
                    setState(focusIndicator == null ? "release" : "kb_release"); // See handleDragOut().
                } else {
                    setState("kb_release");	// * button is not focused and released outside (down->up).
                }
            }
        }
        
        // Controller input
        protected function handlePress(controllerIndex:uint = 0):void {
            if (!enabled) { return; }
            _pressedByKeyboard = true;
            setState(_focusIndicator == null ? "down" : "kb_down");
            
            // Uses single controller like AS2.
            if (autoRepeat && _repeatTimer == null) {
                _autoRepeatEvent = new ButtonEvent(ButtonEvent.CLICK, true, false, controllerIndex, 0, true, true);
                _repeatTimer = new Timer(repeatDelay, 1);
                _repeatTimer.addEventListener(TimerEvent.TIMER_COMPLETE, beginRepeat, false, 0, true);
                _repeatTimer.start();
            }
            
            var sfEvent:ButtonEvent = new ButtonEvent(ButtonEvent.PRESS, true, false, controllerIndex, 0, true, false);
            dispatchEvent(sfEvent);
            
        }
        protected function handleRelease(controllerIndex:uint = 0):void {
            if (!enabled) { return; }
            setState(focusIndicator == null ? "release" : "kb_release");
            
            // Always remove timer, in case autoRepeat was turned off during press.
            if (_repeatTimer) {
                _repeatTimer.stop(); _repeatTimer.reset();
                _repeatTimer.removeEventListener(TimerEvent.TIMER_COMPLETE, beginRepeat);
                _repeatTimer.removeEventListener(TimerEvent.TIMER, handleRepeat);
                _repeatTimer = null; //LM: Consider leaving in memory.
            }
            
            handleClick(controllerIndex);
            _pressedByKeyboard = false;
            
            if (!_isRepeating) {
                var sfEvent:ButtonEvent = new ButtonEvent(ButtonEvent.CLICK, true, false, controllerIndex, 0, true, false);
                dispatchEvent(sfEvent);
            }
            
            _isRepeating = false;
        }
        
        protected function handleClick(controllerIndex:uint = 0):void {
            if (toggle && (!selected || allowDeselect)) { selected = !selected; }
        }
        
        protected function beginRepeat(event:TimerEvent):void {
            _repeatTimer.delay = repeatInterval;
            _repeatTimer.repeatCount = 0;
            _repeatTimer.removeEventListener(TimerEvent.TIMER_COMPLETE, beginRepeat);
            _repeatTimer.addEventListener(TimerEvent.TIMER, handleRepeat, false, 0, true);
            _repeatTimer.reset();
            _repeatTimer.start();
        }
        
        protected function handleRepeat(event:TimerEvent):void {
            // Always remove timer, in case autoRepeat was turned off during press.
            if (_mouseDown == 0 && !_pressedByKeyboard) {
                _repeatTimer.stop(); _repeatTimer.reset();
                _repeatTimer.removeEventListener(TimerEvent.TIMER_COMPLETE, beginRepeat);
                _repeatTimer.removeEventListener(TimerEvent.TIMER, handleRepeat);
                _repeatTimer = null; //LM: Consider leaving in memory.
            }
            
            if (_autoRepeatEvent) { // In theory, mousePressEvent should never be null here.
                _isRepeating = true;
                dispatchEvent(_autoRepeatEvent);
            }
        }
        
        protected function setState(state:String):void {
            _state = state;
            var prefixes:Vector.<String> = getStatePrefixes();
            var states:Array = _stateMap[state];
            if (states == null || states.length == 0) { return; }
            var l:uint = prefixes.length;
            for (var i:uint=0; i<l; i++) {
                var prefix:String = prefixes[i];
                var sl:uint = states.length;
                for (var j:uint=0; j<sl; j++) {
                    var thisLabel:String = prefix + states[j];
                    if (_labelHash[thisLabel]) {
                        _newFrame = thisLabel;
                        invalidateState();
                        return;
                    }
                }
            }
        }
        
        // Optimization to states look-up
        //LM: Make static.
        protected var statesDefault:Vector.<String> = Vector.<String>([""]);
        protected var statesSelected:Vector.<String> = Vector.<String>(["selected_", ""]);
        
        protected function getStatePrefixes():Vector.<String> {
            return _selected ? statesSelected : statesDefault;
        }
        
        // This happens any time the state changes.
        protected function updateAfterStateChange():void {
            if (!initialized) { return; }
            if (constraints != null && !constraintsDisabled && textField != null) {
                constraints.updateElement("textField", textField); // Update references in Constraints 
            }
        }
    }
}